$(function () {
/*
todo:
- merge issue list from this moderator and google code all in to google code
- save css in datastore to get rid of annoying don't leave message
- make tests for typing
- reset styles correctly
- inject javascript that sends class selector back to editor and allows changing.
- fill vertical space with textarea
- load css from current page
- split driver.js into multiple files
- make undo work after sliding
- highlight the text of what's being slid
- context coloring (bespin)
- put red/green/blue boxes in units cell
- make to/from/selector inputs work
- use bespin
- get combo values to work like "border: solid 1px #aaf;"
- if invalid to or from value, just color it red and set to a default shown in blue small text in parens
- persist slider settings like range by selector
- make r, g, b hotkeys for enabling/disabling color flags
- save css in gears/datastore or actual file by editing selenium
- test out css url() values
- give an element count of current selector

- desktop version
    - get chrome to work
    - link scrolling in all browsers
    - get rid of start button appropriately
    - package windows stuff in one exe
    - when opening browsers, go to address in address box
    - make firefox type better as controller
    - allow ie to control itself
    - make safari sliding more smooth    
    
    - production
        - change driver.css to local

*/

var TEST = false;

var CHROME_EXTENSION = chrome.extension ? true : false;

if(CHROME_EXTENSION === true)
{
    var url = window.location.href;
    var tabquery = 'tabid=';
    var tabid = parseInt(url.substring(url.indexOf(tabquery) + tabquery.length));
    var CHROME_SRC_TAB = undefined;
    chrome.tabs.get(tabid, function(tab) {
        CHROME_SRC_TAB = tab;
        cont();
    });
}
else
{
    cont();
}

function cont() {

// selenium driver
var selenium = CHROME_EXTENSION === false ? undefined : function(browser, url) {
    var session = '';
    var self = this;
    
    this.browser = browser;

    // execute a selenese command in the remote browser
    this.cmd = function(args) {
        var data = 'cmd=' + args.name;
        var x = 0;
        for(var i in args.sel_args) {
            x = parseInt(i) + 1;
            data = data + '&' + String(x) + '=' + encodeURIComponent(args.sel_args[i]);
        }
        if(args.name.toLowerCase() !== 'getNewBrowserSession') {
            data = data + '&sessionId=' + this.session;
        }
        
        var ret;
        $.ajax({
            type: "POST",
            url: "http://localhost:4444/selenium-server/driver/",
            data: data,
            async: false,
            success: function (msg) {
                msg_arry = msg.split(',');
                if(msg_arry[0] === 'OK') {
                    if($.isFunction(args.callback)) {
                        args.callback(msg_arry);
                    }
                    ret = msg_arry[1];
                }
                else {
                    throw 'Selenium Error(' + msg + ')\r\r\tCommand(' + data + ')\r\r\tBrowser(' + browser + ')';
                }
            }
        });
        return ret;
    };
    
    // start a new selenium browser session
    this.cmd({
        name: 'getNewBrowserSession', 
        sel_args: [browser, url, ''],
        callback: function (msg_arry) {
            self.session = msg_arry[1];
        }
    });
    
    // execute some javascript in the remote browser
    this.getEval = function(code) {
        var ret;
        if($.isFunction(code)) {
            ret = this.cmd({
                name: 'getEval',
                sel_args: ['(' + String(code) + ')();' ]
            });
        }
        else if(typeof code === 'string') {
            ret = this.cmd({
                name: 'getEval', 
                sel_args: [code]
            });
        }
        return ret;
    };
    
    // close the browser session
    this.stop = function() {
        this.cmd({
            name: 'testComplete'
        });
    };
    
    this.waitForPageToLoad = function(timeout) {
        if(timeout === undefined) {
            timeout = '';
        }
        this.cmd({
            name: 'waitForPageToLoad',
            sel_args: [timeout]
        });
    }
    
    this.waitForCondition = function(code, timeout) {
        if(timeout === undefined) {
            timeout = '';
        }    
        this.cmd({
            name: 'waitForCondition',
            sel_args: [code, timeout]
        });
    }
    
   function debug(str) {
		if (window.console && window.console.log) {
		    window.console.log(str);
		}
	};    
}





// cqstyle
$(function () {
    var HOME_PAGE = 'http://google.com/';
    var TYPE_COLOR = "color";
    var TYPE_NUMBER = "number";
    var TYPE_URL = "url";
    var TAB_PREFIX = "tab-";

    // editor is a singleton, since we only need one
    var editor = {
        id: 'editor1',
        slider_id: 'slider',
        size: 0,
        init: function() {
            //this.load_page(); // won't be neccessary in production build
            this.$text_area = $('#' + this.id);
            
            // link methods of this object to events of the textarea
            this.$text_area.keyup(jQuery.proxy(this, "keyup" ));
            this.$text_area.click(jQuery.proxy(this, "click" ));
            
            // use jquery plugin to keep tabs from changing focus off the textarea and allow tab characters within css
            $(this.$text_area).tabby();
            
            $('#address_go').click(jQuery.proxy(this, "nav" ));
            $('#address').keyup(jQuery.proxy(this, "address_keyup" ));
            $('#address').val(HOME_PAGE);
            
            this.slider = $('#' + this.slider_id).slider({
                value: 0,
                min: 0,
                max: 1000,
                slide: jQuery.proxy(this, "slider_change")
            });
            
            this.$tabs = $('#tabs');
            
            // set red, green, blue slider configs based on checkboxes
            this.set_rgb();
            
            $('.red_check, .green_check, .blue_check').click(jQuery.proxy(this, "set_rgb" ));
            
            this.init_text();
            
            this.load_style_sheets();
        },
        
        load_style_sheets: function()
        {
            var self = this;
            
            browsers.getEval({
                code: 'get_style_sheets();',
                callback: function (result) {
                    if(result.length === undefined) {
                        result.length = 0;
                    }
                    
                    for(var i = 0; i < result.length; i++) {
                        $.ajax({
                            type: "GET",
                            url: result[i],
                            async: false,
                            success: function (msg) {
                                self.add_to_sheets(self, msg, result[i]);
                                if(i === (result.length - 1) ) { // last sheet
                                    self.load_scratch_sheet(self);
                                }
                            }
                        });
                    }
                }
            });
        },
        
        curr_tab_id: TAB_PREFIX + '0',
        tab_mapping: {},
        sheets:  [],
        
        add_to_sheets: function(self, css, url) {
            var index = self.sheets.length;
            self.sheets[index] = css;
            var display_name = url.substring(url.lastIndexOf('/') + 1, url.length);
            
            if(url.length > 25) {
                display_name = display_name.substring(0, 20) + '...';
            }
            
            var id = 'tab-' + index;
            
            self.$tabs.append(
                '<li class="tab tab_dis" id=' + id + '><acronym title="' + url + '">' + display_name + '</acronym></li>');
                
            self.tab_mapping[id] = {
                url: url,
                display_name: display_name,
                css: css
            }
            
            self.tab_click(self, id);
            
            if(index === 0) {
                self.$tabs.children().first().removeClass('tab_dis');
                self.text(css);
                self.curr_tab_id = id;
            }
        },
        
        tab_click: function(self, id) {
            $('#' + id).click(function(event) {
                self.tab_mapping[self.curr_tab_id].css = self.text();
                self.$tabs.children().addClass('tab_dis');
                $('#' + id).removeClass('tab_dis');
                self.text(self.tab_mapping[id].css);
                self.curr_tab_id = id;
            });
        },
        
        load_scratch_sheet: function(self) {
            self.add_to_sheets(self, "", "scratch");
        },
        
        init_text: function()
        {
            $('#editor_label').html("Now editing CSS on " + CHROME_SRC_TAB.url);
        },
        
        // get and set the css text being edited
        text: function(str) {
            if(str === undefined) {
                return this.$text_area.val();
            }
            else {
                this.$text_area.val(str);
                return;
            }
        },
        
        rgb: {
            r: true,
            g: true,
            b: true
        },
        
        set_rgb: function() {          
            this.rgb.r = $('.red_check').attr('checked');
            this.rgb.g = $('.green_check').attr('checked');
            this.rgb.b = $('.blue_check').attr('checked');
            this.set_color_offset(this.slider_value());
            this.sync_slider(this.parsed)
        },
        
        slider_min: function(num) {
            return this.slider_boundary(num, true);
        },
        
        slider_max: function(num) {
            return this.slider_boundary(num, false);
        },
        
        slider_boundary: function(num, min) {
            var type;
            if(min === true) {
                type = 'min';
            }
            else {
                type = 'max';
            }
            if(num !== undefined) {
                $('#' + this.slider_id).slider('option', type, num);
            }
            return $('#' + this.slider_id).slider('option', type);
            
        },
        
        slider_value: function(val) {
            if(val === undefined) {
                return $('#' + this.slider_id).slider('option', 'value');
            }
            else {
                $('#' + this.slider_id).slider('option', 'value', val);
            }
        },
        
        address_keyup: function(event) {
            // if they pressed enter
            if (event.keyCode == 13) {
                this.nav();
            } 
            else {
                return true;
            }
        },
        
        color: {r: 0, g: 0, b: 0},
        /* 
        
        body {background-color: #656565;}
        #logo {margin: 33px;}
        
        */
        
        slider_change: function(event, ui) {
            this.slide(event, ui, browsers.css);
        },
        
        slide: function(event, ui, callback) {
            if(this.from !== '' && this.to !== '') {
                if(this.slider_type === TYPE_COLOR) {
                    ui.value = build_hex_color(
                        this.set_color_from_slider(ui.value)
                    );
                }
                
                var new_value = String(ui.value) + this.parsed.value.units;
                
                //console.debug('new value: ' + new_value + ' start: ' + this.parsed.value.start + ' end: ' + this.parsed.value.end);
                
                // reflect slider change in css
                this.text(
                    insert(
                        this.text(),
                        new_value,
                        this.parsed.value.start,
                        this.parsed.value.end
                    )
                );
                
                // Adjust end position if length of value changed
                if(String(ui.value).length !== this.parsed.value.val.length)
                {
                    this.parsed.value.end = this.parsed.value.end + String(ui.value).length - this.parsed.value.val.length;
                }

                this.parsed.value.val = String(ui.value);
                
                if(callback === undefined) {
                    callback = browsers.send_CSS_property;
                }
                callback({
                    css: {
                        style: this.parsed.block.val,
                        selector: this.parsed.selector.val,
                        property: this.parsed.property.val,
                        value: new_value
                    }
                });
            }
        },
        
        set_color_from_slider: function(val) {
            // set the value, (0 - 255), to all the colors we want to slide
            this.rgb_loop(function(c, self) {
                self.color[c] = valid_color(val + self.color_offset[c]);
            });
            return this.color;
        },
        
        set_color: function(hex_color) {
            var c;
            var code = "rgb";
            for(var i = 0; i < code.length; i++) {
                c = code[i];        
                this.color[c] = valid_color(extract_color(hex_color, c));
            }
            return this.color;
        },
        
        nav: function(e) {
            var address = $('#address').val();
            if(startsWith(address, 'http') === false) {
                address = 'http://' + address;
            }
            
            // Tried making asyc true here, but still didn't get simultaneous loads. 
            // It looks like Selenium threw an error about adding a command before the previous one was finished,
            // so this may not be possible with current selenium build.
            browsers.open(address, false);
            this.send_all();
        },
        
        keyup: function() {
            var old_size = this.size;
            this.text(this.$text_area.val());
            this.size = this.text().length;
            var diff = Math.abs(this.size - old_size);
            if(diff <= 3) {
                this.parse_current();
                console.log(this.parsed);
                this.send_one();
            }
            else {
                this.send_all();
            }
        },
        
        click: function() {
            this.parse_current();
        },
        
        // send the style block that the cursor is currently on
        send_one: function(args) {
            var parsed = this.parsed;
            if(parsed.block.val !== '') {
            
                // send the whole style block
                var map_string = this.get_CSS_block(parsed.block.val);
                
                debug('selector: ' + parsed.selector.val + '; block: ' + map_string);
                
                var refresh = args ? args.refresh: undefined;
                
                browsers.send_CSS_block({
                    selector: parsed.selector.val, 
                    block: map_string,
                    refresh: refresh
                });
            }
        },

        // send all css in text editor
        send_all: function(args) {
            var t = this.text();
            for(var i = 0, j = 0; t !== ''; j++) {
                if(j >= 18000) {
                    alert('cqstyle does not support over 18k styles at a time. Plus it\'s probably a bad idea anyway: ' +
                        'http://www.stevesouders.com/blog/2009/03/10/performance-impact-of-css-selectors/');
                    return;
                }
                var parsed = this.parsed = get_style(t, 0);
                this.send_one(args);
                i = parsed.block.end;
                t = $.trim(t.slice(i + 1));
            }
            this.send_one();
        },
        
        get_CSS_block: function(block) {
            return get_jquery_css_map(block);
        },
                
        rgb_loop: function(callback) {
            var c;
            var code = "rgb";
            for(var i = 0; i < code.length; i++) {
                c = code[i];
                if(this.rgb[c] === true) {
                    callback(c, this);
                }
            }
        },
        
        parse_current: function() {
            var parsed = this.parsed = get_style(this.text(), this.$text_area.getSelection().start - 1);
            
            $('.selector_input').val(parsed.selector.val);
            $('.property_input').val(parsed.property.val);
            $('.value_input').val(parsed.value.val);
            $('.units_input').val(parsed.value.units);
            
            this.sync_slider(parsed);
            
            if(this.slider_type === TYPE_COLOR) {
                this.set_color(parsed.value.val);
                this.set_color_offset(this.slider_value());
            }
            
            return this.parsed;
        },
        
        reset_color_offset: function() {
            this.color_offset = {r: 0, g: 0, b: 0};
        },
        
        color_slider_pos: function(parsed) {
            var count = 0;
            var total = 0;
            this.rgb_loop(function(c, self) {
                count = count + 1;
                total = total + extract_color(parsed.value.val, c);
            });
            return total / count;
        },
        
        set_color_offset: function(pos) {
            this.reset_color_offset();
            this.rgb_loop(function(c, self) {
                self.color_offset[c] = self.color[c] - pos;
            });
        },
        
        sync_slider: function (parsed) {
            if(parsed === undefined || parsed.value === undefined || parsed.value.type === undefined) {
                _default(this);
                return;
            }
            switch(parsed.value.type) {
                case TYPE_COLOR:
                    var pos = this.color_slider_pos(parsed);
                    this.set_color_offset(pos);
                    this.config_slider({
                        min: 0,
                        max: 0xff,
                        value: pos,
                        type: TYPE_COLOR,
                        map: hex_color
                    });
                    break;
                case TYPE_NUMBER:
                    var min = 0;
                    var max = 1000;
                    var value = parsed.value.val;
                    if(parseFloat(parsed.value.val) < 0) {
                        var temp = min;
                        min = max * -1;
                        max = temp;
                    }
                    this.config_slider({
                        min: min,
                        max: max,
                        value: value,
                        type: TYPE_NUMBER
                    });
                    break;
                default:
                    _default(this);
                    break;
            }
            function _default(self) {
                self.config_slider({
                    min: '',
                    max: '',
                    value: 0,
                    type: ''
                });
            }
        },
        
        config_slider: function (args) {
            this.slider_min(args.min);
            this.slider_max(args.max);
            this.slider_value(args.value);
            this.slider_type = args.type;
            if(args.map !== undefined) {
                this.from = args.map(this.slider_min());
                this.to = args.map(this.slider_max());
            }
            else {
                this.from = this.slider_min();
                this.to = this.slider_max();
            }
            
            $('.from_input').val(this.from);
            $('.to_input').val(this.to);
        },
        
        from: 0,
        
        to: 0,
        
        // loads html from development file, so we don't have to re-jar for every change
        load_page: function(callback) {
            // cross-site, JSONP
            $.getJSON("http://localhost:7777/default.aspx/" + "?jsoncallback=?",
                function(data) {
                    $(document.body).prepend(data.k);
                    if($.isFunction(callback)) {
                        callback();
                    }
                }
            );            
        },
        
        css_value: function() {
            return this.parsed.value.val;
        },
        
        set_caret: function (val) {
            this.$text_area.caret(val);
            this.parse_current();
        }
    } // end of editor object
    
    if(CHROME_EXTENSION === true) {
        chrome.extension.onRequest.addListener(
            function(request, sender, sendResponse) {
                if (request.greeting === "refresh") {
                    editor.send_all({refresh: true});
                }
                sendResponse({});
        });
    
        $(editor).extend({ // Overwrite the Selenium based methods
        
            // send all css in text editor
//            send_all: function() {
//                var t = this.text();
//                for(var i = 0, j = 0; t !== ''; j++) {
//                    if(j >= 18000) {
//                        alert('cqstyle does not support over 18k styles at a time. Plus it\'s probably a bad idea anyway: ' +
//                            'http://www.stevesouders.com/blog/2009/03/10/performance-impact-of-css-selectors/');
//                        return;
//                    }
//                    var parsed = this.parsed = get_style(t, 0);
//                    this.send_one();
//                    i = parsed.block.end;
//                    t = $.trim(t.slice(i + 1));
//                }
//                this.send_one();
//            }
        });
    }
    
    function extract_color(hex_rgb, mnuemonic) {
        var start, end;
        switch (mnuemonic) {
            case "r":
                start = 1;
                end = 3;
                break;
            case "g":
                start = 3;
                end = 5;
                break;
            case "b":
                start = 5;
                end = 7;
                break;                
        }
        return parseInt(hex_rgb.substring(start, end), 16);
    }
    
    function build_hex_color(color) {
        var val = 0;
        var code = "rgb";
        var j = code.length - 1;
        // combine colors into one numerical value by bit shifting green 8 bits and red 16
        for(var k in color) {
            c = color[k];
            val = val + (c << (j * 8));
            j = j - 1;
        }
        return hex_color(val);
    }

    function hex_color(num) {
        var hex = num.toString(16) + '';
        while(hex.length < 6) {
            hex = "0" + hex;
        }
        return '#' + hex;
    }      
    
    // input 'text' is the inside of a style like: '{color: #aaa; font-size: 12px; border: solid 1px #000}'
    // output is a string meant for jQuery.css(map);
    function get_jquery_css_map (text) {
        var props = [];
        var i_prop = 0;
        var i = 1; // text[0] = '{' which we don't care about
        var property, value;
        while(i < text.length) {
            // avoid array.push because: http://groups.google.com/group/jquery-dev/browse_thread/thread/5019fa537c8d3595
            property = substring(text, i, ':');
            i = property.end;
            value = get_value(text, i);
            props[i_prop] = {'property': property, 'value': value};
            i = value.end + 1;
            i_prop = i_prop + 1;
        }
        var map = '{';
        for(var j in props) {
            // using regular concatenation, will be slow in IE
            if(parseInt(j) !== 0) {
                map = map + ", ";
            }
            map = map + '"' + props[j].property.val + '": "' + props[j].value.val + props[j].value.units + '"';
        }
        map = map + '}';
        return map;    
    }
    
    function get_css_url(text, pos) {
        var urlregexes = [
            /url\s*\(\s*".*"\s*\)/i,    // url("anything")
            /url\s*\(\s*'.*'\s*\)/i,    // url('anything')
            /url\s*\(.*\)/i             // url(anything)
        ];
        var match = null;
        for(var j in urlregexes) {
            match = text.match(urlregexes[j]);
            if(text.match(urlregexes[j]) !== null) {
                return text.substring(i, i + match.length);
            }
        }
    }
    
    // parses out the selector, block, property, and value from pos in text
    function get_style(text, pos) {
        var c, selector, property, value, style;
        // look backwards since we want to see what was just typed and what we're in the process of editing
        selector = get_selector(text, pos);
        block = get_block(text, selector.end);
        property = {val: '', start: -1, end: -1};
        value = {val: '', units: '', start: -1, end: -1};  // in case we don't set these later   
        for( ; selector.start < pos && pos >= 0; pos--) {
            c = text[pos];
            
            // we think user is editing a proptery name
            if(c === '{' || c === ';') {
                property = substring(text, pos + 1, ':');
                
                // If we got something, then finish (Getting an end curly means we jumped into the next style block,
                // so still no dice)
                if(property.val !== '' && property.val.indexOf('{') === -1) {
                    pos = property.end;
                    value = get_value(text, pos);
                    break;
                }
                // Didn't get anything, so let for loop take us further back
            }
            
            // we think user is editing a property's value
            if(c === ':') {
                value = get_value(text, pos);
                pos = look_back(text, pos, [';', '{']).pos - 1; // jump back to property name
                property = substring(text, pos + 1, ':');
                break;                   
            }     
        }
        return {'selector': selector, 'block': block, 'property': property, 'value': value};
    }
    
    function get_block(text, pos) {
        var block = substring(text, pos, '}');
        block.val = block.val + '}'; // in this case, we want the last character
        return block;
    }
    
    function get_selector(text, pos) {
        // get out of comment blocks
        var beg_next_comment = text.indexOf('/*', pos);
        var end_next_comment = text.indexOf('*/', pos);
        if( 
            // end_next_comment is end of comment currently in, beg_next_comment is a subsequent comment
            (beg_next_comment > end_next_comment) ||
            // end_next_comment is end of comment currently in, no subsequent comments
            (beg_next_comment === -1 && end_next_comment !== -1) ||
            // currently at the beginning of a comment
            (pos === beg_next_comment && end_next_comment !== -1)
        ) {
            // currently in a comment, so get out.
            pos = end_next_comment + 2;
        }
    
        // find end of previous style block or end of some comment
        pos = look_back(text, pos, ['}', '/']).pos; // jump out of the style definition to find the selector
        
        return substring(text, pos + 1, '{');
    }
    
    function look_back(text, pos, chars) {
        var match_char;
        for( ; pos >= 0; pos--) {
            for(var i in chars) {
                if(text[pos] === chars[i]) {
                    match_char = chars[i];
                    if(pos !== 0) {
                        pos = pos + 1; // return position after match character unless beginning of text is reached
                    }
                    return {match_char: match_char, pos: pos};
                }
            }
        }
        return {match_char: undefined, pos: -1};
    }
    
    function get_value(text, pos) {
        var value = substring(text, pos + 1, ';');
        var type = '';
        units = '';
        // Make sure we have a value and didn't leave the style block
        if(value.val === '' || (value.val.indexOf('{') !== -1) ) {
            value = substring(text, pos + 1, '}'); // last declaration
        }
        
        var match;
        if(startsWith(String(value.val), 'url')) {
            value = get_css_url(text, pos);
            type = TYPE_URL;
        }
        else if((match = value.val.match(/^-?\d+/i)) !== null) { // number with units
            var num = match[0];// get zero index for first match
//            if(parseFloat(num) < 0) {
//                value.start = value.start + 1; // don't include negative sign
//                num = num.slice(1);
//            }
            var units = '';
            units = value.val.replace(num, '');
            value.val = num;
            type = TYPE_NUMBER;
        }
        else if((match = value.val.match(/^#([0-9A-Fa-f]{3,6})$/)) !== null) { // hex color
            type = TYPE_COLOR;
        }
        
        // use /^#([0-9A-Fa-f]{6})$/ to test for a color match
        
        return {val: value.val, type: type, units: units, start: value.start, end: value.end};
    }
    
    function substring(str, start, terminator) {
        var pos = start;
        var val;
        for( ; pos < str.length; pos++) {
            if(str[pos] === terminator) {
                val = str.substring(start, pos);
                break;
            }
        }
        if(val === undefined) {
            val = '';
        }
        val = $.trim(val);
        
        while(start < str.length && str[start].match(/\s/) !== null) { //
            start = start + 1;
        }
        
        return {val: val, start: start, end: pos};
    }

    $('#start').click(function() {
        browsers.init(get_checked_browsers());
        $('#start').hide();
        $('#stop').show();
        editor.send_all();
    });
    
    $('#stop').click(function() {
        browsers.stop(get_checked_browsers());
        $('start').show();
    });    
    
    function get_checked_browsers() {
        var ie = false;
        var ff = false;
        var chrome = false;
        if($('#ie_check').attr('checked')) {
            ie = true;
        }
        if($('#ff_check').attr('checked')) {
            ff = true;
        }
        if($('#chrome_check').attr('checked')) {
            chrome = true;
        }
        
        return {
            ie: ie,
            ff: ff,
            chrome: chrome
        }        
    }
    
    
    
    
    
    //// BROWSERS
    //// BROWSERS
    //// BROWSERS
    //// BROWSERS
    //// BROWSERS    
    //// BROWSERS                
    
    
    
    
    
    var browsers;
    
    if(CHROME_EXTENSION === true) {
        browsers = {
            init: function(args) {
                var self = this;
                var url = window.location.href;
                var tabquery = 'tabid=';
                this.tabid = CHROME_SRC_TAB.id;
                console.debug(this.tabid);
                $(window).bind('beforeunload', jQuery.proxy(this, 'beforeunload'));
                
                // jQuery is included via the manifest file
                //this.getEval(get_jquery, 'JQUERY_PATH_PLACEHOLDER', this.jQuery_path);
                
                // getEval callback handler
                chrome.extension.onRequest.addListener(
                    function(request, sender, sendResponse) {
                        console.log(sender.tab.id);
                        console.log(self.tabid);
                        
                        if(request.type === 'getEval' && sender.tab.id === self.tabid) {
                            var index = request.index;
                            if(index !== -1 && self.getEval_queue[index] !== undefined) {
                                var callback = self.getEval_queue[index];
                                callback(request.result);
                            }
                            
                            // Send refresh CSS again because of crazy bug that causes it to fail the first time
                            if(request.refresh === true) {
                                editor.send_all();
                            }
                        }
                        sendResponse({});
                    }
                );
            },
            
            beforeunload: function() {
                return 'You will lose your CSS if you leave.';
            },
            
            getEval: function(args) {
                if($.isFunction(args.code)) {
                    args.code = func_to_str(args.code, args.rep, args.rep_with);
                }
                
                var index = -1;
                if(args.callback !== undefined) {
                    index = this.getEval_queue_length;
                    this.getEval_queue[this.getEval_queue_length] = args.callback;
                    this.getEval_queue_length = this.getEval_queue_length + 1;                    
                }
                
                args.code = 'getEval({' +
                    'code: \'' + args.code
                        .replace(/\'/g, "\\'") // escape single quotes
                        .replace(/\"/g, '\\"') // escape double quotes
                        + '\',' +
                    'index: ' + index +
                '});';
                
                chrome.tabs.executeScript(
                    this.tabid, 
                    {code: args.code},
                    null
                )
            },
            
            getEval_queue: {},
            
            getEval_queue_length: 0,
            
            open: function (address) {
                if(this.address === address) { // Refresh
                    this.get_scroll_pos();
                    this.getEval('window.location = "' + address + '";');
                    // poll for doc ready (steal from jQuery) then do below as callback
                    this.getEval(get_jquery);
                    this.set_scroll_pos();
                }
                else {
                    this.cmd('open', [address]);
                    this.getEval(get_jquery);
                }
                this.address = address;
            },
            
            send_CSS_property: function (args) {
                // used browsers instead of this because callback from slide wasn't passing this correctly
                browsers.getEval({
                    code: 
                        'window.$("' + args.css.selector + '").css(' + '{' + '"' + args.css.property + '" : "' + args.css.value + '"});'
                    }
                );
            },
            
            send_CSS_block: function(args) {
                browsers.getEval({
                    code: 'window.$("' + args.selector + '").css(' + args.block +  ');',
                    refresh: args.refresh
                });             
            },
            
            get_scroll_pos: function() {
                this.scroll_pos = { x: sp[0], y: sp[1] };
            },
            
            set_scroll_pos: function() {
                $(document).ready(function() {
                    this.getEval('window.scrollTo(' + String(b.scroll_pos.x) + ',' + String(b.scroll_pos.y) + ');');
                });
            },
            
            jQuery_path: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js'
            
        }
    }
    else { // Use Selenium
        browsers = {
            all: {}, // selenium objects
            names: {
                ie: '*iexplore',
                ff: '*firefox',
                chrome: '*googlechrome'
            },
            
            init: function(args) {
                this.stop(args);
                for(var i in this.names) {
                    var name = this.names[i];
                    if(args[i] === true) {
                        this.all[i] = new selenium(name, $('#address').val());
                        $(window).bind('beforeunload', jQuery.proxy(this, 'beforeunload'));
                    }                
                }
                
                $('#browser_control').show();
                this.open(HOME_PAGE, false);
            },
            
            beforeunload: function() {
                for(var i in this.all) {
                    this.all[i].stop();
                }
                return 'You will lose your CSS if you leave. If you stay, you made need to still refresh cqstyle to get it working again. Sorry :)';
            },
            
            cmd: function(name, sel_args, callback) {
                for(var i in this.all) {
                    this.all[i].cmd({
                        name: name, 
                        sel_args: sel_args,
                        callback: callback
                    });
                }
            },
            
            getEval: function(args) {
                for(var i in this.all) {
                    //this.all[i].waitForPageToLoad(30000);
                    this.all[i].getEval(args.code);
                }
            },
            
            waitForPageToLoad: function(timeout) {
                for(var i in this.all) {
                    this.all[i].waitForPageToLoad(timeout);
                }            
            },
            
            waitForCondition: function(code, timeout) {
                for(var i in this.all) {
                    this.all[i].waitForCondition(code, timeout);
                }            
            },
            
            open: function (address) {
                if(this.address === address) { // Refresh
                    this.get_scroll_pos();
                    this.getEval({
                        code: 'window.location = "' + address + '";'
                    });
                    // poll for doc ready (steal from jQuery) then do below as callback
                    this.getEval({
                        code: func_to_str(get_jquery, 'JQUERY_PATH_PLACEHOLDER', this.jQuery_path)
                    });
                    this.set_scroll_pos();
                }
                else {
                    this.cmd('open', [address]);
                    this.getEval({
                        code: func_to_str(get_jquery, 'JQUERY_PATH_PLACEHOLDER', this.jQuery_path)
                    });
                }
                this.address = address;
            },
            
            send_CSS_property: function (args) {
                // used browsers instead of this because callback from slide wasn't passing this correctly
                browsers.getEval({
                    code: 'window.$("' + args.css.selector + '").css(' + '{' + '"' + args.css.property + '" : "' + args.css.value + '"});',
                    async: args.asnyc
                });
            },
            
            send_CSS_block: function(args) {
                for(var i in this.all) {
                    b = this.all[i];            
                    b.getEval({
                        code: 'window.$("' + args.selector + '").css(' + args.block +  ');',
                        async: false
                    });
                }
            },            
            
            stop: function(args) {
                for(var i in this.names) {
                    if(args[i] === true && this.all[i] !== undefined) {
                        this.all[i].stop();
                        delete this.all[i];
                    }
                }
                for(var i in this.all) {
                    if(this.all[i] !== undefined) {
                        return;
                    }
                }
                $('#stop').hide();
            },
            
            get_scroll_pos: function() {
                var b;
                var sp;
                for(var i in this.all) {
                    b = this.all[i];
                    sp = b.getEval({
                        code: get_scroll
                    }).split(':');
                    b.scroll_pos = { x: sp[0], y: sp[1] };
                }
            },
            
            set_scroll_pos: function() {
                $(document).ready(function() {
                    var b;
                    for(var i in this.all) {
                        b = this.all[i];
                        b.getEval({
                            code: 'window.scrollTo(' + String(b.scroll_pos.x) + ',' + String(b.scroll_pos.y) + ');'
                        });
                    }            
                });
            },
            
            jQuery_path: 'http://localhost:4444/selenium-server/core/cqstyle/jquery-1.4.min.js'
            
        } // end of browsers object
    }
    
    function func_to_str(func, rep, rep_with) {
        var ret = '(' + String(func) + ')();'
        if(rep !== undefined && rep_with !== undefined) {
            ret = ret.replace(rep, rep_with);
        }
        return ret;
    }
    

    // this function is loaded into target page. it does not have access to closure
    function get_jquery() {
        function check_for_jquery() {
            if(window.jQuery !== undefined) {
                window.clearInterval(ival);
            }
        }
        function include_dom(script_filename) {
            var html_doc = window.document.getElementsByTagName('head').item(0);
            var js = window.document.createElement('script');
            js.setAttribute('language', 'javascript');
            js.setAttribute('type', 'text/javascript');
            js.setAttribute('src', script_filename);
            html_doc.appendChild(js);
            return false;
        }
        
        try {    
            include_dom('JQUERY_PATH_PLACEHOLDER');
        }
        catch (e) {
            throw 'failed to load jQuery into display browser: ERROR: ' + e;
        }
        //alert(String(include_dom));

        var ival = window.setInterval(
            function check_for_jquery() {
                if(window.jQuery !== undefined) {
                    window.clearInterval(ival);
                }
            }
        , 13);

	    // Timeout checker
	    window.setTimeout(function(){
	        if(window.jQuery === undefined) {
		        window.clearInterval(ival);
		        throw 'timed out loading jQuery';
		    }
	    }, 10000); // allow 10 seconds to load jQuery
    }
    
    function get_scroll() {
        var scrOfX = 0;
        var scrOfY = 0;
        if( typeof( window.pageYOffset ) == 'number' ) {
            //Netscape compliant
            scrOfY = window.pageYOffset;
            scrOfX = window.pageXOffset;
        }
        else if( window.document.body && ( window.document.body.scrollLeft || window.document.body.scrollTop ) ) {
            //DOM compliant
            scrOfY = window.document.body.scrollTop;
            scrOfX = window.document.body.scrollLeft;
        }
        else if( window.document.documentElement && 
            ( window.document.documentElement.scrollLeft || window.document.documentElement.scrollTop ) ) {
            //IE6 standards compliant mode
            scrOfY = window.document.documentElement.scrollTop;
            scrOfX = window.document.documentElement.scrollLeft;
        }
        return scrOfX + ':' + scrOfY;
    }
    
    function debug(str) {
	    if (window.console && window.console.log) {
	        window.console.log(str);
	    }
    }      
	
    function startsWith(str, prefix) {
        return !str.indexOf(prefix);
    }
    
    function insert(str, val, start, end) {
        var beg = str.slice(0, start);
        var end = str.slice(end);
        return beg + val + end;
    }
    
    function valid_color(num) {
        return Math.min(255, Math.max(0, num));
    }
    
    browsers.init();
    editor.init();
    
    window.cqstyle = {
        editor: editor,
        browsers: browsers
    }
}); // end of cqstyle object

// tests
$(function() {
    if(location.hash === "#test" || TEST) {
        $('#qunit').show();
        var e = cqstyle.editor;
        var b = cqstyle.browsers;
        
        test("color", function() {
            var css = "body {background-color: #000000;}";
            e.rgb = {
                r: true,
                g: true,
                b: true
            }
            e.text(css);
            
            // test parsing within entire style block
            var caret_str = " caret=";
            var pos_str = "";
            for(var i = 6; i < css.length; i++) {
                pos_str = caret_str + String(i);
                e.set_caret(10);
                equals($('.selector_input').val(), 'body', 'CSS Selector' + pos_str);
                equals($('.property_input').val(), 'background-color', 'Property' + pos_str);
                equals($('.from_input').val(), '#000000', 'From' + pos_str);
                equals($('.to_input').val(), '#0000ff', 'To' + pos_str);               
            }
            
            var prefix = "body {background-color: ";
            var postfix = ";}";
            slide(1, "#010101", prefix, postfix);
            $('.red_check').attr('checked', false);
            e.set_rgb();
            slide(2, "#010202", prefix, postfix);
            slide(-1000, "#010000", prefix, postfix);
            slide(255, "#01ffff", prefix, postfix);
            $('.green_check').attr('checked', false);
            e.set_rgb();
            slide(2, "#01ff02", prefix, postfix);
            slide(-1000, "#01ff00", prefix, postfix);
            slide(255, "#01ffff", prefix, postfix);
            $('.green_check').attr('checked', true);
            $('.blue_check').attr('checked', false);
            e.set_rgb();
            slide(2, "#0102ff", prefix, postfix);
            slide(-1000, "#0100ff", prefix, postfix);
            slide(255, "#01ffff", prefix, postfix);
        });
        
        // regression test
        test("no_color_remember", function() {
            var prefix = "body {background-color: ";
            var postfix = ";}";
            e.rgb = {
                r: true,
                g: true,
                b: true
            }
            e.text('body {background-color: #000000;}');
            e.set_caret(10);
            slide(1, "#010101", prefix, postfix);          
            $('.red_check').attr('checked', false);
            $('.green_check').attr('checked', true);
            $('.blue_check').attr('checked', false);
            e.set_rgb();
            e.text('body {background-color: #000000; color: #aaaaaa;}');               
            e.set_caret(36);
            prefix = 'body {background-color: #000000; color: ';
            slide(255, "#aaffaa", prefix, postfix);
        });
        

        test("margin", function() {
            var css = "body {background-color: #000000; margin: 10px;}";
            var caret_pos_of_margin = css.indexOf('margin:')
            e.text(css);
            e.set_caret(caret_pos_of_margin);       
            var prefix = "body {background-color: #000000; margin: ";
            var postfix = "px;}";
            slide(1, "1", prefix, postfix);
            slide(10, "10", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '0', 'From');
            equals($('.to_input').val(), '1000', 'To');                 
            slide(-1000, "-1000", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '-1000', 'From');
            equals($('.to_input').val(), '0', 'To');                
            slide(1000, "1000", prefix, postfix);
            slide(100, "100", prefix, postfix);  
        });
        
        test("comments", function() {
            var css = "/* a CSS comment */ body {background-color: #000000; margin: 10px;}";
            var caret_pos_of_margin = css.indexOf('margin:')
            e.text(css);
            e.set_caret(caret_pos_of_margin);     
            var prefix = "/* a CSS comment */ body {background-color: #000000; margin: ";
            var postfix = "px;}";
            slide(1, "1", prefix, postfix);
            slide(10, "10", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '0', 'From');
            equals($('.to_input').val(), '1000', 'To');                 
            slide(-1000, "-1000", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '-1000', 'From');
            equals($('.to_input').val(), '0', 'To');                
            slide(1000, "1000", prefix, postfix);
            slide(100, "100", prefix, postfix);  
        });
        
        test("margin_no_space", function() {
            var css = "body {background-color:#000000; margin:10px;}";
            var caret_pos_of_margin = css.indexOf('margin:')
            e.text(css);
            e.set_caret(caret_pos_of_margin);       
            var prefix = "body {background-color:#000000; margin:";
            var postfix = "px;}";
            slide(1, "1", prefix, postfix);
            slide(10, "10", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '0', 'From');
            equals($('.to_input').val(), '1000', 'To');                 
            slide(-1000, "-1000", prefix, postfix);
            e.set_caret(caret_pos_of_margin);
            equals($('.from_input').val(), '-1000', 'From');
            equals($('.to_input').val(), '0', 'To');                
            slide(1000, "1000", prefix, postfix);
            slide(100, "100", prefix, postfix);  
        });
        
        test("typing_no_space", function() {
            var css = 'body {background-color:#000000;}';
            var caret_pos = css.indexOf('background')
            var prefix = "body {background-color:";
            var postfix = ";}";
            e.rgb = {
                r: true,
                g: false,
                b: false
            }
            e.text(css);
            e.set_caret(caret_pos);
            slide(9, "#090000", prefix, postfix);
            e.keyup();
            verify_send_with_comment('9, 0, 0');
            stop();            
        });        
        
        test("typing", function() {
            //start();
            var css = 'body {background-color: #000000;}';
            var caret_pos = css.indexOf('background')
            var prefix = "body {background-color: ";
            var postfix = ";}";
            e.rgb = {
                r: true,
                g: true,
                b: false
            }
            e.text(css);
            e.set_caret(caret_pos);
            slide(256, "#ffff00", prefix, postfix);
            e.keyup();
            verify_send_with_comment('255, 255, 0');
            stop();
        });
                
        test("send_with_comment", function () {
            var prefix = "/* im acomment yo */ body {background-color: rgb(";
            var postfix = ");}";
            e.rgb = {
                r: true,
                g: false,
                b: false
            }
            var carets = [[0, '255, 0, 0']
                        ,[1, '0, 255, 0']
                        ,[5, '0, 0, 255']
                        ,[6, '255, 0, 0']
                        ,[7, '255, 0, 255']
                        ,[19, '0, 255, 255']
                        ,[20, '255, 255, 255']
                        ,[21, '0, 0, 0']];
            
            for(var i = 0; i < carets.length; i++) {
                console.log(prefix + carets[i][1] + postfix);
                e.text(prefix + carets[i][1] + postfix);
                e.set_caret(carets[i][0]);
                e.keyup();
                verify_send_with_comment(carets[i][1]);
                stop();                 
            }
        });
        
        
        
        function verify_send_with_comment(val) {
            b.getEval({
                code: 'window.$("body").css("background-color");',
                callback: function(response) {
                    start();
                    equals(response, 'rgb(' + val + ')', 'body color');
                }
            });            
        }      
        
        
               
        function slide(slide_val, new_value, prefix, postfix) {
            e.slide(undefined, {value: slide_val}, function() {
                equals(e.css_value(), new_value, "Editors's css value after setting slider to " + String(slide_val));
                equals(
                    e.text(),
                    prefix + new_value + postfix,
                    "Editor text after setting slider to " + String(slide_val));
            });                
        }        
    }
}); // end of tests
} // end of cont() function
}); // end of anonymous wrapper namespace